/**!
 * @license FusionCharts JavaScript Library jQuery Plugin v1.0.4
 * Copyright FusionCharts Technologies LLP
 * License Information at <http://www.fusioncharts.com/license>
 *
 * @author FusionCharts Technologies LLP
 */
/**
 * HTMLTable Data Handler
 * This module maps chart type alias with particular FusionCharts object.
 * @private
 *
 * @module fusioncharts.transcoder.htmltable
 */
FusionCharts.register('module', ['private', 'HTMLTableDataHandler', function () {

    var global = this,
        win = global.window,
        doc = win.document,
        /**
         *  Used to remove all textNodes in an array of nodes. Textnodes get created
         *  inadvertently, when the table HTML has newline and space. Returns an
         *  array of nodes that is not an HTMLCollection and has only the non-text
         *  nodes in it.
         *
         *  @param {array} nodeArr Contains the array of nodes that have to be
         *  sanitized.
         */
        sanitizeNodesArray = function (nodeArr) {
            var l, i, sanitizedArr = [];
            for (i = 0, l = nodeArr.length; i < l; i += 1) {
                // if nodeType != 3 then the node is not a text node.
                if (nodeArr[i].nodeType !== 3) {
                    sanitizedArr.push(nodeArr[i]);
                }
            }
            return sanitizedArr;
        },


        /**
         * Used to merge two JSON objects. The copy is a deep copy and not a
         * reference copy.
         *
         * @param: {object} srcJSON, the source json.
         *
         * @param: {object} targetJSON, that json that needs to be merged with the
         * source json.
         *
         */
        mergeJSON = function (srcJSON, targetJSON) {
            var i,
                len,
                item;

            if (targetJSON instanceof Array) {
                for (i = 0, len = targetJSON.length; i < len; i++) {
                    if (targetJSON[i] instanceof Array) {
                        if (srcJSON[i] === undefined) {
                            srcJSON[i] = [];
                        }
                        mergeJSON(srcJSON[i], targetJSON[i]);
                    } else if (typeof targetJSON[i] === 'object') {
                        if (typeof srcJSON[i] === 'undefined') {
                            srcJSON[i] = {};
                        }
                        mergeJSON(srcJSON[i], targetJSON[i]);
                    } else {
                        srcJSON[i] = targetJSON[i];
                    }
                }
            } else if (typeof targetJSON === 'object') {
                for (item in targetJSON) {
                    if (typeof srcJSON[item] === 'undefined') {
                        if (targetJSON[item] instanceof Array) {
                            srcJSON[item] = [];
                            mergeJSON(srcJSON[item], targetJSON[item]);
                        } else if (typeof srcJSON[item] === 'object') {
                            srcJSON[i] = {};
                            mergeJSON(srcJSON[i], targetJSON[i]);
                        } else {
                            srcJSON[i] = targetJSON[i];
                        }
                    }
                }
            }

            return srcJSON;
        },

        /**
         *  Returns the <tbody> element of the table. In order to support W3C
         *  non-compliant HTML, wherein the table does not have a <tbody> containing
         *  the <tr> & <td> elements, the table itself is returned.
         *
         *  @param {object} tbl Is the table whose tbody needs to fetched.
         *
         */
        getTbody = function (tbl) {
            var tbodyArr = sanitizeNodesArray(tbl.childNodes);

            // Checking if the table's childNode is a tbody or not
            if (tbodyArr.length) {
                if (tbodyArr[0].nodeName === 'TBODY') {
                    return tbodyArr[0];
                }
                else if (tbodyArr[0].nodeName === 'THEAD' && tbodyArr[1] && tbodyArr[1].nodeName === 'TBODY') {
                    return tbodyArr[1];
                }
            }
            return tbl;
        },

        /**
         *  Returns an array containing the <td> in the <thead> tag.
         *
         *  @param {object} tbl Is the table whose tbody needs to fetched.
         *
         */
        getThead = function (tbl) {
            var tbodyArr = sanitizeNodesArray(tbl.childNodes);

            // Checking if the table's childNode is a tbody or not
            if (tbodyArr.length) {
                if (tbodyArr[0].nodeName === 'THEAD' && tbodyArr[1] && tbodyArr[1].nodeName === 'TBODY') {
                    return tbodyArr[0].childNodes;
                }
            }
            return [];
        },

        /**
         *  Used to return the text in a given Element. Needed as firefox does not
         *  support 'innerText', which is a W3C standard and instead uses
         *  textContent attribute to store the text.
         *
         *  @param {object} nodeEle The HTML element whose text is needed.
         */
        getTextFromNode = function (nodeEle) {
            return (nodeEle.innerText !== undefined) ? nodeEle.innerText : nodeEle.textContent;
        },

        /**
         *  Used to convert an array of rows into an array of corresponding columns.
         *
         *  @param {array} rowArr The array of table rows that has to be transposed
         *  into an array of columns.
         */
        getColumnArr = function (rowArr) {

            var i,
                j,
                l,
                len,
                cellArr,
                rowSpanInc,
                colSpan = 1,
                columnArrIdx,
                rowSpan = {},
                returnObj = [];

            for (i = 0, l = rowArr.length; i < l; i += 1) {

                // get all the cells of the row
                cellArr = sanitizeNodesArray(rowArr[i].childNodes);
                colSpan = 1;
                rowSpanInc = 0;
                for (j = 0, len = cellArr.length; j < len; j += 1) {

                    // if a previous row had a cell with a rowspan then this
                    // rowspan needs to be taken into account while creating
                    // column array.
                    columnArrIdx = j + colSpan + rowSpanInc - 1;
                    if (rowSpan[columnArrIdx] &&
                            ((i - rowSpan[columnArrIdx].rowNum) < rowSpan[columnArrIdx].row)) {
                        rowSpanInc += rowSpan[columnArrIdx].col;
                        columnArrIdx += rowSpan[columnArrIdx].col;
                    }

                    if (parseInt(cellArr[j].getAttribute('rowspan'), 10) > 1) {
                        if (!rowSpan[columnArrIdx]) {
                            rowSpan[columnArrIdx] = {};
                        }
                        rowSpan[columnArrIdx].rowNum = i;
                        rowSpan[columnArrIdx].row = parseInt(cellArr[j].getAttribute('rowspan'), 10);
                        // If the cell has both a rowspan and a colspan then they
                        // both need to be taken into consideration while calculating
                        // the column of cells in the susequent rows
                        if (parseInt(cellArr[j].getAttribute('colspan'), 10) > 1) {
                            rowSpan[columnArrIdx].col = parseInt(cellArr[j].getAttribute('colspan'), 10);
                        } else {
                            rowSpan[columnArrIdx].col = 1;
                        }
                    }

                    // The previous cells colspan, and the previous rows rowspans
                    // also needs to be added to the current cells index to get the
                    // proper column index.
                    while (returnObj.length <= columnArrIdx) {
                        returnObj.push({childNodes: []});
                    }
                    returnObj[columnArrIdx].childNodes.push(cellArr[j]);

                    // Adding the current cells colspan for subsequent cells in the
                    // current row.
                    if (parseInt(cellArr[j].getAttribute('colspan'), 10) > 1) {
                        colSpan += parseInt(cellArr[j].getAttribute('colspan'), 10) - 1;
                    }
                }
            }

            return returnObj;
        },

        /**
         * Used to check if an item is present in an array or not.
         *
         * @param {array} arr The array which has to be checked
         *
         * @param {string|number|object} item The item which needs be checked if
         * present in array arr or not.
         */
        arrayContains = function (arr, item) {
            var i = arr.length;
            // Using a decrementing while loop (optimization) since the order in which the
            // array is traversed doesn't matter.
            while (i) {
                i -= 1;
                if (arr[i] === item) {
                    return true;
                }
            }
            return false;
        },

        /**
         * Used to check if a particular row or column has all non numeric (or blank)
         * content. If so, then that particular row/column must be ignored.
         *
         * @param {array} nodeArr Is an array of all the rows/columns of the table.
         *
         * @param {number} index of the row/column that is not to be analysed as it
         * has already been set aside as the label row/column.
         *
         * @param {number} j is the cell index at which we have to check for the
         * presence of numeric data in all the other rows and columns.
         */

        checkData = function (nodeArr, j, index) {

            var i,
                l,
                childArr = sanitizeNodesArray(nodeArr[j].childNodes),
                text;

            for (i = 0, l = childArr.length; i < l; i += 1) {
                if (i !== index) {
                    text = getTextFromNode(childArr[i]);
                    if (parseFloat(text) === text) {
                        return true;
                    }
                }
            }
            return false;
        },

        _blankString = '__fcBLANK__',
        _blankNo = 0,
        /**
         * Used to get the table row that has the labels (categories or legend) and
         * extract the label details from the corresponding row.
         *
         * @param {array} nodeArr Is an array of all the rows/columns of the table.
         *
         * @param {array} ignoreArr Is an array of all the indexes in the nodeArr
         * that need to be ignored. The items of this array can be negative as well.
         *
         * @param {string} index If the label row/column details have been given by
         * the user then this will contain the index which has the labels.
         *
         */
        getLabels = function (nodeArr, ignoreArr, index, opts) {

            var len, l, i, j, childArr, mostEmptyCellRow = null, internalLabel = [],
                emptyCellCount = [], textCellCount = 0, temp, returnObj = {},
                spanTotal = 0, spanLen, isRowLabel, maxIdx, spanLength, totalSpanLength = 0, tLabels;


            if (typeof index === 'undefined') {
            // Checking if the user has provided the index. If index has not been
            // passed then we assume that the user does not want to give a label row
            // or column.

                // Creating custom labels for all cells in the first row/column.
                childArr = sanitizeNodesArray(nodeArr[0].childNodes);
                for (j = 0, len = childArr.length; j < len; j += 1) {
                    spanLen = j + spanTotal;
                    internalLabel[spanLen] = _blankString + (spanLen + 1);

                    temp = parseInt(childArr[j].colSpan, 10);
                    temp = (temp > 1) ? temp : parseInt(childArr[j].rowSpan, 10);
                    if (temp > 1) {
                        for (l = 1; l < temp; l += 1) {
                            internalLabel[spanLen + l] = _blankString + (spanLen + l + 1);
                        }
                        spanTotal += (temp - 1);
                    }
                }

                // Deleting the labels for the rows/columns that the user wants to
                // ignore.
                for (i = 0, l = (j + spanTotal), len = ignoreArr.length; i < len; i += 1) {
                    if (ignoreArr[i] > 0) {
                        delete internalLabel[ignoreArr[i] - 1];
                    } else {
                        delete internalLabel[l + ignoreArr[i]];
                    }
                }

                return {'index': -1, 'labelObj': internalLabel};

            } else if (index === 0) {
                // Checking if the user has provided the index. Since we expect row/column
                // indices starting from 1, if index is 0 then we use our internal logic
                // to find the label array from the given nodeArr.
                for (i = 0, l = nodeArr.length; i < l; i += 1) {
                    childArr = sanitizeNodesArray(nodeArr[i].childNodes);
                    emptyCellCount[i] = 0;
                    textCellCount = 0;

                    if(opts && opts._extractByHeaderTag){
                        for (j = 0, len = childArr.length; j < len; j += 1) {
                            if(childArr[j].nodeName.toLowerCase() != 'th'){
                                continue;
                            }
                            tLabels = getLabels(nodeArr, ignoreArr, i + 1);
                            delete tLabels.labelObj[opts._rowLabelIndex];
                            return tLabels;
                        }
                    }else{
                        for (j = 0, len = childArr.length; j < len; j += 1) {
                            if (arrayContains(ignoreArr, (j + 1)) ||
                                    arrayContains(ignoreArr, (j - len))) {
                                continue;
                            }
                            temp = getTextFromNode(childArr[j]);
                            // Checking if the cell is emtpy.
                            if (temp.replace(/^\s*/, '').replace(/\s*$/, '') === '') {
                                emptyCellCount[i] += 1;
                                continue;
                            }
                            // Checking if the cell has a non-number content
                            if (parseFloat(temp) != temp) {
                                textCellCount += 1;
                                // If there are at least 2 cells that have non-number
                                // content then we assume that they contain labels and
                                // fetch the labels from this array of nodes.
                                if (textCellCount > 1) {
                                    return getLabels(nodeArr, ignoreArr, i + 1);
                                }
                            }
                        }
                    }
                    // If there are empty cells then we assume that the array with
                    // the most number of empty cells must be the label array.
                    if (i > 0) {
                        if (emptyCellCount[i - 1] > emptyCellCount[i]) {
                            mostEmptyCellRow = i - 1;
                        } else if (emptyCellCount[i - 1] < emptyCellCount[i]) {
                            mostEmptyCellRow = i;
                        }
                    }
                }
                if (mostEmptyCellRow !== null) {
                    return getLabels(nodeArr, ignoreArr, mostEmptyCellRow + 1);
                } else {
                    return getLabels(nodeArr, ignoreArr);
                }
            }

            // If this is a negative number then, calulate the index from the
            // end of the table. e.g -1 would imply the last row.
            if (index < 0) {
                index += nodeArr.length;
            } else if (index > 0) {
                index -= 1;
            }

            /**
             * Once we have the index of the row/column that contains the labels we
             * go through only that row/column and extract the labels.
             */
            childArr = sanitizeNodesArray(nodeArr[index].childNodes);

            isRowLabel = (nodeArr[0].nodeType !== undefined) ? true : false;
            for (j = 0, len = childArr.length; j < len; j += 1) {
                spanLength = 0;
                if (isRowLabel) {
                    if (childArr[j].colSpan !== '1') {
                        spanLength = parseInt(childArr[j].colSpan, 10);
                    }
                } else if (childArr[j].rowSpan !== '1') {
                    spanLength = parseInt(childArr[j].rowSpan, 10);
                }
                spanLength = (spanLength > 1) ? spanLength : 0;
                temp = getTextFromNode(childArr[j]);
                if (temp.replace(/^\s*/, '').replace(/\s*$/, '') !== '') {
                    returnObj[j + totalSpanLength] = temp;
                } else if (checkData(getColumnArr(nodeArr), j, index)) {
                    // if the label text is missing in one of the cells of the labels
                    // row/column we check for the presence of numeric data in other
                    // cells of the corresponding column/row.

                    returnObj[j + totalSpanLength] = _blankString + _blankNo;
                    _blankNo += 1;
                }

                if (spanLength > 1) {
                    // If the spanLength > 1, then we create additional labels for
                    // rows/columns pertaining to the rowspan or colspan.
                    temp = returnObj[j + totalSpanLength];
                    for (i = 1; i < spanLength; i += 1) {
                        returnObj[j + totalSpanLength + i] = temp + ' (' + i + ')';
                    }

                    // totalSpanLength is used to adjust the indices of the
                    // subsequent cells using the rowspan/colspan of the current
                    // cell.
                    totalSpanLength += (spanLength - 1);
                }
            }

            // Deleting the rows/columns that the user wants to ignore.
            maxIdx = len + totalSpanLength;
            for (i = 0, len = ignoreArr.length; i < len; i += 1) {
                if (ignoreArr[i] > 0) {
                    delete returnObj[ignoreArr[i] - 1];
                } else {
                    delete returnObj[maxIdx + ignoreArr[i]];
                }
            }

            return {'labelObj': returnObj, 'index': index};
        },

        extractDataFromTable = function (tbl, opts) {

            if (typeof tbl === 'string') {
                tbl = doc.getElementById(tbl);
            }

            if ((typeof win.jQuery !== 'undefined') && (tbl instanceof win.jQuery)) { // jshint ignore: line
                tbl = tbl.get(0);
            }

            if (!tbl) {
                return {data: null};
            }

            if (opts.hideTable) {
                tbl.style.display = 'none';
            }

            var i,
                j,
                rowCells,
                cellText,
                dataMap = {},
                mapColumnIdx,
                columnSpan,
                len,
                item,
                rowSpan,
                cellEle,
                columnSpanObj = {},
                rowSpanObj = {},
                tableRows = sanitizeNodesArray(getThead(tbl)).concat(sanitizeNodesArray(getTbody(tbl).childNodes)),
                l = tableRows.length,
                dataRows = 0,
                dataColumns = 0,
                tempColumn = 0,
                rowLabelMap,
                m,
                k = 0,
                columnLabelMap,
                isSingleSeries = false,
                chartType = opts.chartType,
                tempMap,

                singleSeriesCharts = ['column2d', 'column3d', 'pie3d', 'pie2d',
                                        'line', 'bar2d', 'area2d', 'doughnut2d',
                                        'doughnut3d', 'pareto2d', 'pareto3d'];


            if(singleSeriesCharts.indexOf(chartType) !== -1){
                isSingleSeries = true;
            }



            // use rowLabelSource and colLabelSource to avoid confusion
            opts.rowLabelSource = parseInt(opts.labelSource, 10);
            opts.colLabelSource = parseInt(opts.legendSource, 10);
            // Create the labels objects for the chart.

            if (opts.major === 'column') {
                rowLabelMap = opts.useLabels ?
                    getLabels(tableRows, opts.ignoreCols, opts.rowLabelSource) :
                    getLabels(tableRows, opts.ignoreCols);

                columnLabelMap = opts.useLegend ?
                    getLabels(getColumnArr(tableRows), opts.ignoreRows, opts.colLabelSource) :
                    getLabels(getColumnArr(tableRows), opts.ignoreRows);
            }else{
                tempMap = getLabels(getColumnArr(tableRows), opts.ignoreRows, opts.rowLabelSource);
                if(!opts.useLabels){
                    rowLabelMap = getLabels(getColumnArr(tableRows), opts.ignoreRows);
                }else{
                    rowLabelMap = tempMap;
                }

                opts._rowLabelIndex = tempMap.index;
                opts._extractByHeaderTag = true;

                columnLabelMap = opts.useLegend ?
                    getLabels(tableRows, opts.ignoreCols, opts.colLabelSource, opts) :
                    getLabels(tableRows, opts.ignoreCols);

                delete opts._extractByHeaderTag;

                tempMap = rowLabelMap;
                rowLabelMap = columnLabelMap;
                columnLabelMap = tempMap;
            }

            delete rowLabelMap.labelObj[columnLabelMap.index];
            delete columnLabelMap.labelObj[rowLabelMap.index];

            // Creating the 2d map depending on whether the rows are the primary
            // keys or the columns.
            if (opts.major === 'row') {
                for (item in columnLabelMap.labelObj) {
                    dataMap[item] = {};
                }
            } else {
                for (item in rowLabelMap.labelObj) {
                    dataMap[item] = {};
                }
            }

            // Populating the dataMap.
            for (i = 0; i < l; i += 1) {
                if (rowLabelMap.index === i ||
                        (columnLabelMap.labelObj[i] === undefined)) {
                    continue;
                }

                dataRows += 1;
                rowCells = sanitizeNodesArray(tableRows[i].childNodes);

                // columnSpanObj maintains the number of colspans in the current
                // row.
                // rowSpanObj maintains the number of rowspans in a rows x columns
                // map.
                columnSpanObj[i] = 0;
                rowSpanObj[i] = {};

                for (j = 0, len = rowCells.length; j < len; j += 1) {

                    cellEle = rowCells[j];

                    columnSpan = parseInt(cellEle.getAttribute('colspan'), 10);
                    rowSpan = parseInt(cellEle.getAttribute('rowspan'), 10);

                    mapColumnIdx = j + columnSpanObj[i];

                    // Calculating the position of the current cell in the dataMap.
                    while (k < i) {
                        if (rowSpanObj[k]) {
                            for (m in rowSpanObj[k]) {
                                if (m > mapColumnIdx) {
                                    break;
                                }

                                if ((i - k) <= rowSpanObj[k][m].row) {
                                    mapColumnIdx += rowSpanObj[k][m].col;
                                }
                            }
                        }
                        k += 1;
                    }

                    if (columnSpan > 1) {
                        columnSpanObj[i] += (columnSpan - 1);
                    }

                    if (rowSpan > 1) {
                        if (columnSpan > 1) {
                            rowSpanObj[i][mapColumnIdx] = {
                                row: (rowSpan - 1),
                                col: columnSpan
                            };
                        } else {
                            rowSpanObj[i][mapColumnIdx] = {
                                row: (rowSpan - 1),
                                col: 1
                            };
                        }
                    }
                    if (columnLabelMap.index === mapColumnIdx ||
                        (rowLabelMap.labelObj[mapColumnIdx] === undefined)) {

                        continue;
                    }

                    tempColumn += 1;
                    cellText = getTextFromNode(cellEle);
                    // If the cell does not have any text then we covert it by
                    // default to 0 or to an parameterized option set by user.
                    if (cellText.replace(/^\s*/, '').replace(/\s*$/, '') === '') {
                        if (opts.convertBlankTo) {
                            cellText = opts.convertBlankTo;
                        } else {
                            continue;
                        }
                    }

                    // Filling up the dataMap based on the rowspan, colspan and
                    // position(row=i, column=mapColumnIdx) information of the
                    // table cell.
                    columnSpan = (columnSpan > 1) ? columnSpan : 1;
                    rowSpan = (rowSpan > 1) ? rowSpan : 1;

                    if (opts.major === 'row') {
                        k = 0;
                        while (k < columnSpan) {
                            m = 0;
                            while (m < rowSpan) {
                                dataMap[i + m][mapColumnIdx + k] = parseFloat(cellText);
                                m += 1;
                            }
                            k += 1;
                        }
                    }
                    else {
                        k = 0;
                        while (k < columnSpan) {
                            m = 0;
                            while (m < rowSpan) {
                                dataMap[mapColumnIdx + k][i + m] = parseFloat(cellText);
                                m += 1;
                            }
                            k += 1;
                        }
                    }
                }
                if (tempColumn > dataColumns) {
                    dataColumns = tempColumn;
                }
            }

            return {
                data: dataMap,
                chartType: chartType ? (!isSingleSeries ? 'multi' : 'single') :
                    ((dataRows > 1 && dataColumns > 1) ? 'multi' : 'single'),
                labelMap: columnLabelMap,
                legendMap: rowLabelMap
            };
        },

        createChartFromTable = function (data, obj, config) {

            // Default configuration for HTMLTable data-handler
            var opts = {
                chartAttributes: { },

                major: 'row',
                useLabels: true,
                useLegend: true,
                labelSource: 0,
                legendSource: 0,
                ignoreCols: [],
                ignoreRows: [],
                showLabels: true,
                showLegend: true,
                seriesColors: [],
                convertBlankTo: '0',
                hideTable: false,
                chartType: obj.chartType && obj.chartType(),

                // Private Variables
                labels: [],
                legend: [],
                data: []
            }, i, item1, item2, categoryArr, datasetArr, chartJSON = {},
            datasets = {}, dataObj, dataMap, labelMap, legendMap;

            global.extend(opts, config);

            dataObj = extractDataFromTable(data, opts);
            dataMap = dataObj.data;

            if (opts.major !== 'row') {
                labelMap = dataObj.legendMap,
                legendMap = dataObj.labelMap;
            } else {
                labelMap = dataObj.labelMap,
                legendMap = dataObj.legendMap;
            }

            // chartAttributes should contain the configuration attributes for the chart
            // e.g caption, xAxisName, yAxisName etc.
            chartJSON.chart = global.extend({}, opts.chartAttributes);

            if (dataObj.chartType === 'multi') {
                chartJSON.categories = [{'category': []}];
                chartJSON.dataset = [];

                categoryArr = chartJSON.categories[0].category;
                datasetArr = chartJSON.dataset;

                i = 0;
                for (item1 in dataMap) {

                    if (opts.showLabels === true) {
                        // If the user has provided custom labels then those should be
                        // shown instead of the extracted labels.
                        categoryArr.push(global.extend({
                            label: (labelMap.labelObj[item1].indexOf(_blankString) != -1) ?
                                '' : labelMap.labelObj[item1]
                        },
                        opts.labels[i]));
                    } else {
                        categoryArr.push({'label': ''});
                    }
                    i += 1;
                    for (item2 in dataMap[item1]) {
                        if (typeof datasets[item2] === 'undefined') {
                            datasets[item2] = [];
                        }
                        datasets[item2].push({'value': dataMap[item1][item2]});
                    }
                }

                i = 0;
                for (item1 in datasets) {
                    if (opts.showLegend === true) {
                        // If the user has provided custom labels then those should be
                        // shown instead of the extracted labels.
                        datasetArr.push(global.extend({
                            'seriesname': ((legendMap.labelObj[item1].indexOf(_blankString) !== -1) ?
                                '' : legendMap.labelObj[item1]),
                            'data': datasets[item1]
                        }, opts.legend[i]));
                    } else {
                        datasetArr.push({
                            'seriesname': '',
                            'data': datasets[item1]
                        });
                    }
                    i += 1;
                }
            } else if (dataObj.chartType === 'single') {
                chartJSON.data = [];
                datasetArr = chartJSON.data;

                i = 0;
                if (opts.showLabels) {
                    for (item1 in dataMap) {
                        for (item2 in dataMap[item1]) {
                            datasetArr.push(global.extend({
                                    label: ((labelMap.labelObj[item1].indexOf(_blankString) !== -1) ?
                                        '' : labelMap.labelObj[item1]),
                                    value: dataMap[item1][item2]
                                },
                                opts.labels[i])
                            );

                            i += 1;
                        }
                    }
                } else {
                    for (item1 in dataMap) {
                        for (item2 in dataMap[item1]) {
                            datasetArr.push({'value': dataMap[item1][item2]});
                        }
                    }
                }
            }

            return {
                data: global.core.transcodeData(chartJSON, 'JSON', 'XML'),
                error: undefined
            };
        };

    global.addDataHandler('HTMLTable', {
        encode: function (data, obj, config) {
            return createChartFromTable(data, obj, config);
        },
        decode: function (data, obj) {
            /**
             * @private
             *
             * @typedef {RuntimeException} Error-07101734
             * @memberOf FusionCharts.debugger
             * @group debugger-error
             */
            global.raiseError(obj, '07101734', 'run', '::HTMLTableDataHandler.decode()',
                'FusionCharts HTMLTable data-handler only supports decoding of data.');
            throw new Error('FeatureNotSupportedException()');
        },
        transportable: false
    });
}]);
/**
 * @module fusioncharts.jqueryplugin
 * @private
 * @requires fusioncharts.transcoder.htmltable
 *
 * @export fusioncharts.jqueryplugin.js
 */
FusionCharts.register('module', ['private', 'extensions.jQueryPlugin', function () {

    var global = this,
        win = global.window,
        lib = global.hcLib,
        doc = win.document,
        jQ = win.jQuery,
        renderFusionCharts,
        captureAllFusionChartsEvents,
        getChartObjectsFromSelection,
        configureLinkedCharts,
        math = win.Math,
        mathMin = math.min,
        isArray = lib.isArray,
        realtimeCommandMap = {
            feed: 'feedData',
            setdata: 'setData',
            setdataforid: 'setDataForId',
            getdata: 'getData',
            getdataforid: 'getDataForId',
            clear: 'clearChart',
            stop: 'stopUpdate',
            start: 'restartUpdate'
        },
        optionsParser = {
            feedData: function (options) {
                if (typeof options === 'string') {
                    return [options];
                }
                else if (typeof options === 'object' && options.stream) {
                    return [options.stream];
                }
                else {
                    return false;
                }
            },
            getData: function (options) {
                // index is passed in case of multivalue charts.
                if (!isNaN(options)) {
                    return [options];
                }
                else if (typeof options === 'object' && options.index) {
                    return [options.index];
                }
                else {
                    return [];
                }
            },
            getDataForId: function (options) {
                // index is passed in case of multivalue charts.
                if (typeof options === 'string') {
                    return [options];
                }
                else if (typeof options === 'object' && options.id) {
                    return [options.id];
                }
                else {
                    return [];
                }
            },
            setData: function (options, value, label) {
                var arr = [];
                if (typeof options !== 'object') {
                    arr = [options, value, label];
                }
                else {
                    options.value && arr.push(options.value);
                    options.label && arr.push(options.label);
                }

                return arr;
            },
            setDataForId: function (options, value, label) {
                var arr = [];
                if (typeof options === 'string' || typeof value === 'string' ||
                        typeof label === 'string') {
                    arr = [options, value, label];
                }
                else if (typeof options === 'object') {
                    options.value && arr.push(options.value);
                    options.label && arr.push(options.label);
                }

                return arr;
            },
            clearChart: function (options) {
                return [options];
            },
            stopUpdate: function (options) {
                return [options];
            },
            restartUpdate: function (options) {
                return [options];
            }
        };

    // Assign FusionCharts object to global jQuery object for easy use.
    jQ.FusionCharts = global.core;

    /**
     * Used purely for rendering the FusionCharts.
     * Acts as a common method that is invoked by all the APIs that create a
     * FusionChart object
     *
     * @param {array} elemList is a list of all the HTML elements that
     * are selected using the jQuery selectors
     *
     * @param {object} chartOptions is the options that are to be passed to
     * the FusionCharts contructor.
     */

    renderFusionCharts = function (elemList, chartOptions) {
        var i,
            l,
            cO,
            chartObj,
            scriptElement;

        if (isArray(chartOptions) || chartOptions instanceof jQ) {
            l = mathMin(elemList.length, chartOptions.length);
        }
        else {
            l = elemList.length;
        }

        for (i = 0; i < l; i += 1) {

            // One-to-one mapping with the HTML elements in case of
            // multiple option objects.
            if (isArray(chartOptions) || chartOptions instanceof jQ) {
                cO = chartOptions[i];
            } else {
                cO = chartOptions;
            }

            // check if the element is appended to the window document or not.
            if (elemList[i].parentNode) {
                // Adding the renderAt option to the chartOptions tells the
                // constructor where to render the FusionCharts object.
                global.core.render(jQ.extend({}, cO, {
                    renderAt: elemList[i]
                }));
            } else {
                chartObj = new FusionCharts(jQ.extend({}, cO, {
                    renderAt: elemList[i]
                }));

                if (!jQ.FusionCharts.delayedRender) {
                    jQ.FusionCharts.delayedRender = {};
                }
                jQ.FusionCharts.delayedRender[chartObj.id] = elemList[i];

                scriptElement = doc.createElement('script');
                scriptElement.setAttribute('type', 'text/javascript');

                if (/msie/i.test(win.navigator.userAgent) && !win.opera) {
                    scriptElement.text = 'FusionCharts.items[\'' + chartObj.id + '\'].render();';
                } else {
                    scriptElement.appendChild(
                        doc.createTextNode('FusionCharts.items[\'' + chartObj.id + '\'].render()')
                    );
                }
                elemList[i].appendChild(scriptElement);
            }
        }
        return elemList;
    };

    /**
     *  Using jQuery's event model for attaching handlers to FusionCharts events.
     *  This is achieved by listening to the FusionCharts "*" event and then
     *  triggering a jQuery event on the associated DOM element.
     */
    captureAllFusionChartsEvents = function (event, args) {
        var containerElement;

        // Extending our event with the jQuery event model for proper
        // delegation and bubbling.
        jQ.extend(event, jQ.Event('fusioncharts' + event.eventType));

        // Checking if there is an associated DOM object
        if (event.sender && event.sender.options) {
            containerElement = event.sender.options.containerElement ||
                event.sender.options.containerElementId;

            if (typeof containerElement === 'object') {
                jQ(containerElement).trigger(event, args);
            }
            else if (jQ('#' + containerElement).length) {
                jQ('#' + containerElement).trigger(event, args);
            }
            else {
                jQ(doc).trigger(event, args);
            }
        } else {
            // If there is no DOM object associated with the FusionCharts object
            // then triggering the event on the document itself for any possible
            // global handlers that might want to capture it.
            jQ(doc).trigger(event, args);
        }
    };

    global.addEventListener('*', captureAllFusionChartsEvents);

    /**
     * Used to select all the HTML object/embed elements that have been created
     * using the FusionCharts constructor
     *
     * @param {jQuery} obj, the selection of elements that need to be processed.
     *
     */
    getChartObjectsFromSelection = function (obj) {

        // The HTML object/embed may be part of the current selection or a
        // child of the current selection. Need to take both cases into account.
        // @note If the FusionCharts object has not been rendered yet, e.g in
        // case the container is not appended to the document, then 'find' for
        // that element will NOT return the corresponding FusionCharts object.
        return obj.filter(':FusionCharts').add(obj.find(':FusionCharts'));
    };

    /**
     *  Used to configure the links at various levels in a linked chart.
     *
     *  @param {jQuery} chartObjects The FusionCharts objects for which the link
     *  has to be configured.
     *
     *  @param {object} linkConfigObj contains the configuration details of the
     *  linked chart like swfUrl, height, width etc.
     *
     *  @param {string} level contains the level at which the user wants to
     *  configure the link.
     *
     */
    configureLinkedCharts = function (chartObjects, linkConfigObj, level) {

        if (typeof linkConfigObj === 'object') {
            chartObjects.each(function () {
                this.configureLink(linkConfigObj, level);
            });
        }
    };

    /**
     * @id: jQuery.fn.insertFusionCharts
     * @id: $.fn.insertFusionCharts
     *
     * @param {object} options contains the parameters that need to be passed
     * to the FusionCharts constructor
     *
     * Inserts the FusionCharts objects in the HTML elements that are selected
     * by the jQuery selector.
     */
    jQ.fn.insertFusionCharts = function (options) {
        return renderFusionCharts(this, options);

    };

    /**
     * @id: jQuery.fn.appendFusionCharts
     * @id: $.fn.appendFusionCharts
     *
     * @param {object} options contains that parameters that need to be passed
     * to the FusionCharts constructor
     *
     * Appends the FusionCharts objects immediately after the HTML elements
     * that are selected by the jQuery selector.
     */
    jQ.fn.appendFusionCharts = function (options) {
        options.insertMode = 'append';
        return renderFusionCharts(this, options);
    };

    /**
     * @id: jQuery.fn.prependFusionCharts
     * @id: $.fn.prependFusionCharts
     *
     * @param {object} options contains the parameters that need to be passed
     * to the FusionCharts constructor
     *
     *  Prepends the FusionCharts objects before the HTML elements that are
     *  selected by the jQuery selector.
     */
    jQ.fn.prependFusionCharts = function (options) {
        options.insertMode = 'prepend';
        return renderFusionCharts(this, options);
    };

    /**
     * @id: jQuery.fn.attrFusionCharts
     * @id: $.fn.attrFusionCharts
     *
     * @param {object|string} attr, If this is a string then it contains
     * the FusionCharts object's attribute that needs to be set or fetched.
     * If it is an object then, it contains the attributes along with the
     * corresponding values that need to be set on the FusionCharts object
     *
     * @param {string} attrVal, To be used if attr is a string. Contains the
     * value that needs to be set for the attribute that attr corresponds to.
     *
     *  Used to set or get the attribute(s) of the FusionCharts object.
     */
    jQ.fn.attrFusionCharts = function (attr, attrVal) {

        /**
         * @ignore
         * @todo Remove ignore
         *
         * @var {jQuery} chartsObjects stores the FusionCharts objects in
         * the selected HTML elements.
         * @var {object} transfer Holds all atttributes to be returned to the
         * callee Function.
         */
        var transfer = [],
            chartObjects = getChartObjectsFromSelection(this);

        if (attrVal !== undefined) {
            // Set the charts attribute attr with value attrVal.

            chartObjects.each(function () {
                this.FusionCharts.setChartAttribute(attr, attrVal);
            });
            return this;
        }
        if (typeof attr === 'object') {
             // Set the charts attributes, in the passed object's keys with
             // the corresponding values.

            chartObjects.each(function () {
                this.FusionCharts.setChartAttribute(attr);
            });
            return this;
        }

        // If both the above cases fail, user is trying to, in accordance with the
        // jQuery paradigm, get the value of the arrtibute.
        chartObjects.each(function () {
            transfer.push(
                this.FusionCharts.getChartAttribute(attr)
            );
        });
        return transfer;
    };

    /**
     * @id jQuery.fn.updateFusionCharts
     * @id $.fn.updateFusionCharts
     *
     * @param {object} options Contains the new options that the FusionCharts
     * objects need to update themselves with. Currently, using this interface
     * the dataType, data, width, height, debugMode and swfUrl can be updated.
     */
    jQ.fn.updateFusionCharts = function (options) {

        var filterOpts = {},
            chartObjects = getChartObjectsFromSelection(this),
            updateOptions = [
                ['swfUrl', false],
                ['type', false],
                ['height', false],
                ['width', false],
                ['containerBackgroundColor', true],
                ['containerBackgroundAlpha', true],
                ['dataFormat', false],
                ['dataSource', false]
            ],
            i,
            l,
            fcChart,
            renderFlag,
            optStr,
            newChart;

        for (i = 0, l = updateOptions.length; i < l; i += 1) {
            optStr = updateOptions[i][0];
            filterOpts.type = filterOpts.type || filterOpts.swfUrl;
            if (options[optStr]) {
                if (updateOptions[i][1]) {
                    renderFlag = true;
                }
                filterOpts[optStr] = options[optStr];
            }
        }

        chartObjects.each(function () {
            // If height and width are given then resize the chart first.
            fcChart = this.FusionCharts;
            if (renderFlag) {
                newChart = fcChart.clone(filterOpts);
                newChart.render();
                return;
            }

            if (filterOpts.dataSource !== undefined || filterOpts.dataFormat !== undefined) {
                if (filterOpts.dataSource === undefined) {
                    fcChart.setChartData(fcChart.args.dataSource, filterOpts.dataFormat);
                } else if (filterOpts.dataFormat === undefined) {
                    fcChart.setChartData(filterOpts.dataSource, fcChart.args.dataFormat);
                } else {
                    fcChart.setChartData(filterOpts.dataSource, filterOpts.dataFormat);
                }
            }
            if (filterOpts.width !== undefined || filterOpts.height !== undefined) {
                fcChart.resizeTo(filterOpts.width, filterOpts.height);
            }
            if (filterOpts.type) {
                fcChart.chartType(filterOpts.type);
            }
        });

        return this;
    };

    /**
     *  @id: jQuery.fn.cloneFusionCharts
     *  @id: $.fn.cloneFusionCharts
     *
     *  @param {object} options The options object that takes the additional
     *  parameters to be passed while cloning the FusionCharts object.
     *
     *  @param {function} callback The callback function that has to be called
     *  once the FusionCharts objects have been cloned. This function will take
     *  the new clone objects as parameter.
     *
     */
    jQ.fn.cloneFusionCharts = function (callback, options) {

        var transfer,
            temp,
            chartObjects;

        // Check if the options parameter, which is not mandatory, has been
        // passed or not. If not, that means that options is the callback function.
        if (typeof callback !== 'function' && typeof options === 'function') {
            temp = callback;
            callback = options;
            options = temp;
        }

        transfer = [];
        chartObjects = getChartObjectsFromSelection(this);

        chartObjects.each(function () {
            transfer.push(this.FusionCharts.clone(options, {}, true));
        });

        callback.call(jQ(transfer), transfer);

        return this;
    };

    /**
     *  @id: jQuery.fn.disposeFusionCharts
     *  @id: $.fn.disposeFusionCharts
     *
     */
    jQ.fn.disposeFusionCharts = function () {

        var chartObjects = getChartObjectsFromSelection(this);

        chartObjects.each(function () {
            // Execute dispose on charts.
            this.FusionCharts.dispose();

            // Remove identifier reference variable
            delete this.FusionCharts;

            // cleanup any static objects pertaining to FusionCharts.
            if (this._fcDrillDownLevel === 0) {
                delete this._fcDrillDownLevel;
            }
        });

        return this;
    };

    /**
     * @id jQuery.fn.covertToFusionCharts
     * @id $.fn.convertToFusionCharts
     *
     * @param {object} chartOpts Configuration options to generate FusionCharts.
     * See documentation to get the list.
     *
     * @param {object} convertOpts Configuration options to convert the table
     * into a FusionCharts object.
     * See documentation to get the list.
     *
     */
    jQ.fn.convertToFusionCharts = function (chartOpts, convertOpts) {

        var transferObj = [];

        if (typeof chartOpts.dataConfiguration === 'undefined') {
            chartOpts.dataConfiguration = {};
        }

        jQ.extend(true, chartOpts.dataConfiguration, convertOpts);

        if (!chartOpts.dataSource) {
            chartOpts.dataSource = this.get(0);
        }

        if (!chartOpts.renderAt) {
            this.each(function () {
                transferObj.push(jQ('<div></div>')
                    .insertBefore(this)
                    .insertFusionCharts(chartOpts).get(0)
                );
            });
        }
        else {
            if (typeof chartOpts.renderAt === 'string') {
                transferObj.push(jQ('#' + chartOpts.renderAt)
                    .insertFusionCharts(chartOpts).get(0)
                );
            }
            else if (typeof chartOpts.renderAt === 'object') {
                transferObj.push(jQ(chartOpts.renderAt)
                    .insertFusionCharts(chartOpts).get(0)
                );
            }
        }

        return jQ(transferObj);
    };

    /**
     * @id jQuery.fn.drillDownFusionChartsTo
     * @id $.fn.drillDownFusionChartsTo
     *
     * Used to set multi-level configurations of linked FusionCharts objects.
     * The levels are iterated depending on the number of configuration objects
     * in a single jQuery chain.
     *
     * To set the configuration at a specific level please refer to docs.
     */
    jQ.fn.drillDownFusionChartsTo = function () {
        var chartObjects = getChartObjectsFromSelection(this),
            j,
            len,
            i,
            l,
            configureOpts;


        // hack to support chaining of multiple drillDowns in a single chain
        if (typeof this._fcDrillDownLevel === 'undefined') {
            this._fcDrillDownLevel = 0;
        }

        for (j = 0, len = arguments.length; j < len; j += 1) {
            configureOpts = arguments[j];

            if (isArray(configureOpts)) {
                for (i = 0, l = configureOpts.length; i < l; i += 1) {
                    configureLinkedCharts(chartObjects, configureOpts[i], this._fcDrillDownLevel);
                    this._fcDrillDownLevel += 1;
                }
            } else {
                configureLinkedCharts(chartObjects, configureOpts, this._fcDrillDownLevel);
                this._fcDrillDownLevel += 1;
            }
        }
        return this;
    };

    /**
     * @id jQuery.fn.streamFusionChartsData
     * @id $.fn.streamFusionChartsData
     *
     * @param {string} command. Contains the type of operation to be performed on
     * the realtime charts. If not provided, by default the command would be 'feed'
     * that invokes the feedData method of the chart and passes the options to it.
     *
     * @param {object} options. Contains the options that has to be parsed (if it is a js object)
     * and passed to the chart method (corresponding to command).
     * If not an object, it is passed as is to the chart method.
     *
     * @param {object} value. To make the API morph the data setter methods (setData, setDataForId),
     * it can also be passed the same parameters as the setter functions.
     *
     * @param {object} label. To make the API morph the data setter methods (setData, setDataForId),
     * it can also be passed the same parameters as the setter functions.
    **/

    jQ.fn.streamFusionChartsData = function (command, options, value, label) {

        var chartObjects = getChartObjectsFromSelection(this),
            transfer = [],
            fcChart,
            method,
            params;

        // Convert the command to lower case and fetch the proper chart method name.
        method = realtimeCommandMap[command && command.toLowerCase()];

        // Check if the command provided is valid or not.
        if (method === undefined) {
            // this means the command is not a getter. which in turn means that
            // the command is a data stream string and has to be handled accordingly.
            if (arguments.length === 1) {
                params = [command];
                method = realtimeCommandMap.feed;
            }
            else {
                return this;
            }
        }
        else if (arguments.length === 1) {
            // command is to invoke a method without any parameters..
            params = [];
        }
        else {
            // optionsParser returns an array of parameters to be passed to the
            // chart method.
            params = optionsParser[method](options, value, label);
        }

        if (method === 'getData' || method === 'getDataForId') {
            chartObjects.each(function () {
                fcChart = this.FusionCharts;
                if (typeof fcChart[method] === 'function') {
                    transfer.push(fcChart[method].apply(fcChart, params));
                }
            });
            return transfer;
        }
        else {
            chartObjects.each(function () {
                fcChart = this.FusionCharts;
                if (typeof fcChart[method] === 'function') {
                    fcChart[method].apply(fcChart, params);
                }
            });
            return this;
        }
    };

    jQ.extend(jQ.expr[':'], {
        /**
         *  Extending the jQuery selector to select all object/embed elements that
         *  have been created using the FusionCharts constructor i.e that are an
         *  instance of FusionCharts.
         *
         *  @param {object} obj, Is the HTML element that is currently being
         *  checked.
         */
        FusionCharts: function (obj) {
            return (obj.FusionCharts instanceof global.core);
        }
    });

}]);
 /**
  * @module fusioncharts.jqueryplugin
  * @private
  * @requires ../../../vendors/fusioncharts-jquery-plugin/package/fusioncharts-jquery-plugin.js
  *
  * @export fusioncharts.jqueryplugin.js
  */
